feat: initial ito-markets SDK for PyPI#1
Conversation
Typed Python client for the Ito Markets public API. Includes: - Baskets, Markets, Data, and Backtests resource namespaces - Automatic retry with exponential backoff on 429/5xx - Typed exceptions (ItoAuthError, ItoRateLimitError, etc.) - Context manager support - CI workflow (pytest + ruff on Python 3.10-3.12) - PyPI publish workflow via Trusted Publishers (OIDC) Distribution name: ito-markets (pip install ito-markets) Import name: ito (from ito import ItoClient) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| if params: | ||
| spec["params"] = params | ||
| if execution: | ||
| spec["execution"] = execution | ||
| if budgets: | ||
| spec["budgets"] = budgets | ||
| if basket_id: | ||
| spec["basket_id"] = basket_id |
There was a problem hiding this comment.
Falsy check silently drops explicitly-passed empty dicts. A caller who intentionally passes
params={}, execution={}, or budgets={} to override defaults (or to conform to an API schema that expects the key present) will have those values silently omitted from the request body. if params is not None: is the correct guard — it distinguishes "user explicitly passed an empty dict" from "user passed nothing".
| if params: | |
| spec["params"] = params | |
| if execution: | |
| spec["execution"] = execution | |
| if budgets: | |
| spec["budgets"] = budgets | |
| if basket_id: | |
| spec["basket_id"] = basket_id | |
| if params is not None: | |
| spec["params"] = params | |
| if execution is not None: | |
| spec["execution"] = execution | |
| if budgets is not None: | |
| spec["budgets"] = budgets | |
| if basket_id is not None: | |
| spec["basket_id"] = basket_id |
| base_url=self._base_url, | ||
| headers={ | ||
| "Authorization": f"Bearer {api_key}", | ||
| "User-Agent": "ito-python/0.1.0", |
There was a problem hiding this comment.
The version string in the
User-Agent header is hardcoded to "ito-python/0.1.0" and will fall out of sync with __version__ in src/ito/__init__.py on every release. Import and use the package version instead.
| "User-Agent": "ito-python/0.1.0", | |
| "User-Agent": f"ito-python/{__import__('ito').__version__}", |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| DEFAULT_TIMEOUT = 30.0 | ||
| MAX_RETRIES = 3 | ||
| RETRY_BACKOFF_FACTOR = 1.0 | ||
| RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504} |
There was a problem hiding this comment.
429 is already a member of RETRYABLE_STATUS_CODES but is also handled by the dedicated if response.status_code == 429: branch above, making the set entry dead code. Including it here creates the false impression that the generic retry path handles rate limiting, when it never reaches that check for 429. Removing it from the set makes the intent explicit.
| RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504} | |
| RETRYABLE_STATUS_CODES = {500, 502, 503, 504} |
Summary
Initial public release of the Ito Markets Python SDK. Distribution name
ito-marketson PyPI (pip install ito-markets), import asfrom ito import ItoClient.Included:
ItoClientwith four resource namespaces:baskets(9 endpoints),markets(3),data(2),backtests(9)HttpTransportwith automatic retry + exponential backoff on 429/5xxItoAPIError->ItoAuthError,ItoRateLimitError,ItoNotFoundError,ItoValidationErrorpy.typedmarkerWhy
ito-marketsnotito: Theitoname on PyPI is taken by an unrelated package (rexsutton/vollab). The Python import name remainsito.To publish after merge: create a GitHub Release (e.g.
v0.1.0) after configuring a Trusted Publisher on PyPI.Link to Devin session: https://app.devin.ai/sessions/ef08e8d7517d4afcaf2492b519ab9a1a
Requested by: @alejandorosumah-mansa
Greptile Summary
This PR introduces the initial public release of the
ito-marketsPython SDK, providing a typed, synchronous client (ItoClient) over the Ito Markets REST API with automatic retry/backoff, a typed exception hierarchy, and 21 respx-mocked tests.HttpTransportinsrc/ito/_http.pyhandles retries with exponential backoff on 429/5xx;429is included inRETRYABLE_STATUS_CODESbut is dead code there since the explicit== 429branch always executes first.Backtests._build_specuses falsy truthiness checks (if params:) rather thanif params is not None:, silently dropping any explicitly-passed empty dict — a caller who passesparams={}to explicitly clear defaults will have that key omitted from the request body.Backtests.run()does a baresubmission["data"]["run_id"]key access that will surface as an opaqueKeyErrorif the API response doesn't match the expected shape.Confidence Score: 4/5
Safe to merge with the _build_spec truthiness fix applied; the silent-drop of empty dicts is the only issue that can cause wrong API payloads.
The backtest spec builder drops any explicitly-passed empty dict (params, execution, budgets) because it uses
if params:instead ofif params is not None:, meaning callers who pass{}to deliberately omit a key or reset defaults will silently get the wrong request body. The rest of the SDK — transport, error handling, resource methods, CI/publish workflows — looks solid.src/ito/resources/backtests.py (_build_spec truthiness checks and run() key access) and src/ito/_http.py (User-Agent version drift, dead 429 entry in RETRYABLE_STATUS_CODES).
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant User participant ItoClient participant HttpTransport participant ItoAPI User->>ItoClient: client.backtests.run(strategy_id, ...) ItoClient->>HttpTransport: post("/backtests/atop", body) loop Retry (up to max_retries) HttpTransport->>ItoAPI: POST /backtests/atop alt 429 Rate Limit ItoAPI-->>HttpTransport: 429 + Retry-After HttpTransport->>HttpTransport: sleep(retry_after or backoff) else 5xx Transient ItoAPI-->>HttpTransport: 500/502/503/504 HttpTransport->>HttpTransport: sleep(backoff) else Network Error HttpTransport->>HttpTransport: catch TransportError, sleep(backoff) else Success ItoAPI-->>HttpTransport: "200 {data: {run_id}}" HttpTransport-->>ItoClient: dict end end loop Poll until done ItoClient->>HttpTransport: "get("/backtests/atop/{run_id}")" HttpTransport->>ItoAPI: "GET /backtests/atop/{run_id}" ItoAPI-->>HttpTransport: "200 {data: {status}}" HttpTransport-->>ItoClient: result dict alt "status == succeeded/failed/error" ItoClient-->>User: final result else still running ItoClient->>ItoClient: sleep(poll_interval) end end%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant User participant ItoClient participant HttpTransport participant ItoAPI User->>ItoClient: client.backtests.run(strategy_id, ...) ItoClient->>HttpTransport: post("/backtests/atop", body) loop Retry (up to max_retries) HttpTransport->>ItoAPI: POST /backtests/atop alt 429 Rate Limit ItoAPI-->>HttpTransport: 429 + Retry-After HttpTransport->>HttpTransport: sleep(retry_after or backoff) else 5xx Transient ItoAPI-->>HttpTransport: 500/502/503/504 HttpTransport->>HttpTransport: sleep(backoff) else Network Error HttpTransport->>HttpTransport: catch TransportError, sleep(backoff) else Success ItoAPI-->>HttpTransport: "200 {data: {run_id}}" HttpTransport-->>ItoClient: dict end end loop Poll until done ItoClient->>HttpTransport: "get("/backtests/atop/{run_id}")" HttpTransport->>ItoAPI: "GET /backtests/atop/{run_id}" ItoAPI-->>HttpTransport: "200 {data: {status}}" HttpTransport-->>ItoClient: result dict alt "status == succeeded/failed/error" ItoClient-->>User: final result else still running ItoClient->>ItoClient: sleep(poll_interval) end endComments Outside Diff (1)
src/ito/resources/backtests.py, line 811 (link)submission["data"]["run_id"]will raise an opaqueKeyErrorif the API ever returns a successful 2xx with an unexpected body shape (e.g., during a schema change or when a validation message is returned withoutrun_id). Consider falling back with a clearer error:run_id = submission.get("data", {}).get("run_id")followed by a check thatrun_idis truthy, raisingItoAPIErrorwith a descriptive message if it isn't.Reviews (1): Last reviewed commit: "Merge initial SDK setup into main" | Re-trigger Greptile